package jdos.hardware;


public class Adlib {

    static final private class Timer {
        double start;
        double delay;
        boolean enabled, overflow, masked;
        /*Bit8u*/short counter;
        Timer() {
            masked = false;
            overflow = false;
            enabled = false;
            counter = 0;
            delay = 0;
        }
        //Call update before making any further changes
        void Update( double time ) {
            if ( !enabled || delay==0 )
                return;
            double deltaStart = time - start;
            //Only set the overflow flag when not masked
            if ( deltaStart >= 0 && !masked ) {
                overflow = true;
            }
        }
        //On a reset make sure the start is in sync with the next cycle
        void Reset(double time ) {
            overflow = false;
            if ( delay==0 || !enabled )
                return;
            double delta = (time - start);
            double rem = delta % delay;
            double next = delay - rem;
            start = time + next;
        }
        void Stop( ) {
            enabled = false;
        }
        void Start(double time, /*Bits*/int scale ) {
            //Don't enable again
            if ( enabled ) {
                return;
            }
            enabled = true;
            delay = 0.001 * (256 - counter ) * scale;
            start = time + delay;
        }

    }

    private static final class Chip {
        //Last selected register
        Timer[] timer = new Timer[2];
        public Chip() {
            for (int i=0;i<timer.length;i++) {
                timer[i] = new Timer();
            }
        }
        //Check for it being a write to the timer
        boolean Write(/*Bit32u*/int addr, /*Bit8u*/short val) {
            switch ( addr ) {
            case 0x02:
                timer[0].counter = val;
                return true;
            case 0x03:
                timer[1].counter = val;
                return true;
            case 0x04:
                double time = Pic.PIC_FullIndex();
                if ((val & 0x80)!=0) {
                    timer[0].Reset( time );
                    timer[1].Reset( time );
                } else {
                    timer[0].Update( time );
                    timer[1].Update( time );
                    if ((val & 0x1)!=0) {
                        timer[0].Start( time, 80 );
                    } else {
                        timer[0].Stop( );
                    }
                    timer[0].masked = (val & 0x40) > 0;
                    if ( timer[0].masked )
                        timer[0].overflow = false;
                    if ((val & 0x2)!=0) {
                        timer[1].Start( time, 320 );
                    } else {
                        timer[1].Stop( );
                    }
                    timer[1].masked = (val & 0x20) > 0;
                    if ( timer[1].masked )
                        timer[1].overflow = false;

                }
                return true;
            }
            return false;
        }
        //Read the current timer state, will use current double
        /*Bit8u*/short Read() {
            double time = Pic.PIC_FullIndex();
            timer[0].Update( time );
            timer[1].Update( time );
            /*Bit8u*/short ret = 0;
            //Overflow won't be set if a channel is masked
            if ( timer[0].overflow ) {
                ret |= 0x40;
                ret |= 0x80;
            }
            if ( timer[1].overflow ) {
                ret |= 0x20;
                ret |= 0x80;
            }
            return ret;
        }
    }

//The type of handler this is
    static final private int MODE_OPL2 = 0;
    static final private int MODE_DUALOPL2 = 1;
    static final private int MODE_OPL3 = 2;

    static public interface Handler {
        //Write an address to a chip, returns the address the chip sets
        public /*Bit32u*/long WriteAddr( /*Bit32u*/int port, /*Bit8u*/short val );
        //Write to a specific register in the chip
        public void WriteReg( /*Bit32u*/int addr, /*Bit8u*/short val );
        //Generate a certain amount of samples
        public void Generate( Mixer.MixerChannel chan, /*Bitu*/int samples );
        //Initialize at a specific sample rate and mode
        public void Init( /*Bitu*/long rate );
    }

//The cache for 2 chips or an opl3
//    typedef /*Bit8u*/short RegisterCache[512];
    
    public final static int oplrate = 44100;
    
    static private class Module {
        private IoHandler.IO_ReadHandleObject[] ReadHandler = new IoHandler.IO_ReadHandleObject[3];
        private IoHandler.IO_WriteHandleObject[] WriteHandler = new IoHandler.IO_WriteHandleObject[3];
        private Mixer.MixerObject mixerObject = new Mixer.MixerObject();

        public Module() {
            for (int i=0;i<ReadHandler.length;i++)
                ReadHandler[i] = new IoHandler.IO_ReadHandleObject();
            for (int i=0;i<WriteHandler.length;i++)
                WriteHandler[i] = new IoHandler.IO_WriteHandleObject();
            for (int i=0;i<chip.length;i++) {
                chip[i] = new Chip();
            }
            reg.normal = 0;
//            capture = null;

            /*Bitu*/int base = SBlaster.sbBase;//section.Get_hex("sbbase").toInt();
            /*Bitu*/int rate = oplrate;//section.Get_int("oplrate");
            //Make sure we can't select lower than 8000 to prevent fixed point issues
            if ( rate < 8000 )
                rate = 8000;
            String oplemu = "default";//section.Get_string( "oplemu" );//{ "default", "compat", "fast"};

            mixerChan = mixerObject.Install(OPL_CallBack,rate,"FM");
            mixerChan.SetScale(2.0f);
            if (oplemu.equals("fast")) {
                handler = new DbOPL.Handler();
            } else if (oplemu.equals("compat")) {
                System.out.println("OPLEMU compat not implemented");
//                if ( oplmode == OPL_opl2 ) {
//                    handler = new OPL2::Handler();
//                } else {
//                    handler = new OPL3::Handler();
//                }
                handler = new DbOPL.Handler();
            } else {
                handler = new DbOPL.Handler();
            }
            handler.Init( rate );
            boolean single = false;
            switch ( oplmode ) {
            case Hardware.OPL_opl2:
                single = true;
                Init(MODE_OPL2);
                break;
            case Hardware.OPL_dualopl2:
                Init(MODE_DUALOPL2);
                break;
            case Hardware.OPL_opl3:
                Init(MODE_OPL3);
                break;
            }
            //0x388 range
            WriteHandler[0].Install(0x388,OPL_Write,IoHandler.IO_MB, 4 );
            ReadHandler[0].Install(0x388,OPL_Read,IoHandler.IO_MB, 4 );
            //0x220 range
            if ( !single ) {
                WriteHandler[1].Install(base,OPL_Write,IoHandler.IO_MB, 4 );
                ReadHandler[1].Install(base,OPL_Read,IoHandler.IO_MB, 4 );
            }
            //0x228 range
            WriteHandler[2].Install(base+8,OPL_Write,IoHandler.IO_MB, 2);
            ReadHandler[2].Install(base+8,OPL_Read,IoHandler.IO_MB, 1);

//            MAPPER_AddHandler(OPL_SaveRawEvent,MK_f7,MMOD1|MMOD2,"caprawopl","Cap OPL");
        }

        //Mode we're running in
        private int mode;
        //Last selected address in the chip for the different modes
        static private class Reg {
            /*Bit32u*/int normal;
            /*Bit8u*/short dual(int index) {
                if (index == 0)
                    return (short)(normal & 0xFF);
                else
                    return (short)((normal >> 8) & 0xFF);
            }
            /*Bit8u*/void dual(int index, int value) {
                if (index == 0) {
                    normal &= 0xFFFFFF00;
                    normal |= value & 0xFF;
                } else {
                    normal &= 0xFFFF00FF;
                    normal |= (value << 8) & 0xFF;
                }
            }
        }
        static private Reg reg = new Reg();
        private void CacheWrite( /*Bit32u*/int reg, /*Bit8u*/short val ) {
            //capturing?
//            if ( capture ) {
//                capture.DoWrite( reg, val );
//            }
            //Store it into the cache
            cache[ reg ] = val;
        }
        private void DualWrite( /*Bit8u*/short index, /*Bit8u*/short reg, /*Bit8u*/short val ) {
            //Make sure you don't use opl3 features
            //Don't allow write to disable opl3		
            if ( reg == 5 ) {
                return;
            }
            //Only allow 4 waveforms
            if ( reg >= 0xE0 ) {
                val &= 3;
            }
            //Write to the timer?
            if ( chip[index].Write( reg, val ) )
                return;
            //Enabling panning
            if ( reg >= 0xc0 && reg <=0xc8 ) {
                val &= 0x0f;
                val |= index!=0 ? 0xA0 : 0x50;
            }
            /*Bit32u*/int fullReg = reg + (index!=0 ? 0x100 : 0);
            handler.WriteReg( fullReg, val );
            CacheWrite( fullReg, val );
        }
        public static int oplmode = Hardware.OPL_none;
        public Mixer.MixerChannel mixerChan;
        public /*Bit32u*/long lastUsed;				//Ticks when adlib was last used to turn of mixing after a few second

        public Handler handler;				//Handler that will generate the sound
//        public RegisterCache cache;
        public short[] cache = new short[512];
//        public Capture capture;
        public Chip[] chip = new Chip[2];

        //Handle port writes
        public void PortWrite(/*Bitu*/int port, /*Bitu*/short val, /*Bitu*/int iolen) {
            //Keep track of last write time
            lastUsed = Pic.PIC_Ticks;
            //Maybe only enable with a keyon?
            if ( !mixerChan.enabled ) {
                mixerChan.Enable(true);
            }
            if ((port & 1)!=0) {
                switch ( mode ) {
                case MODE_OPL2:
                case MODE_OPL3:
                    if ( !chip[0].Write( reg.normal, val ) ) {
                        handler.WriteReg( reg.normal, val );
                        CacheWrite( reg.normal, val );
                    }
                    break;
                case MODE_DUALOPL2:
                    //Not a 0x??8 port, then write to a specific port
                    if ((port & 0x8)==0) {
                        /*Bit8u*/short index = (short)(( port & 2 ) >> 1);
                        DualWrite( index, reg.dual(index), val );
                    } else {
                        //Write to both ports
                        DualWrite( (short)0, reg.dual(0), val );
                        DualWrite( (short)1, reg.dual(1), val );
                    }
                    break;
                }
            } else {
                //Ask the handler to write the address
                //Make sure to clip them in the right range
                switch ( mode ) {
                case MODE_OPL2:
                    reg.normal = (int)handler.WriteAddr( port, val ) & 0xff;
                    break;
                case MODE_OPL3:
                    reg.normal = (int)handler.WriteAddr( port, val ) & 0x1ff;
                    break;
                case MODE_DUALOPL2:
                    //Not a 0x?88 port, when write to a specific side
                    if ((port & 0x8)==0) {
                        /*Bit8u*/int index = ( port & 2 ) >> 1;
                        reg.dual(index, val & 0xff);
                    } else {
                        reg.dual(0, val & 0xff);
                        reg.dual(1, val & 0xff);
                    }
                    break;
                }
            }
        }

        public /*Bitu*/int PortRead(/*Bitu*/int port, /*Bitu*/int iolen)  {
            switch ( mode ) {
            case MODE_OPL2:
                //We allocated 4 ports, so just return -1 for the higher ones
                if ((port & 3)==0) {
                    //Make sure the low /*Bits*/int are 6 on opl2
                    return chip[0].Read() | 0x6;
                } else {
                    return 0xff;
                }
            case MODE_OPL3:
                //We allocated 4 ports, so just return -1 for the higher ones
                if ((port & 3)==0) {
                    return chip[0].Read();
                } else {
                    return 0xff;
                }
            case MODE_DUALOPL2:
                //Only return for the lower ports
                if ((port & 1)!=0) {
                    return 0xff;
                }
                //Make sure the low /*Bits*/int are 6 on opl2
                return chip[ (port >> 1) & 1].Read() | 0x6;
            }
            return 0;
        }
        public void Init(int m) {
            mode = m;
            switch ( mode ) {
            case MODE_OPL3:
            case MODE_OPL2:
                break;
            case MODE_DUALOPL2:
                //Setup opl3 mode in the hander
                handler.WriteReg(0x105, (short)1);
                //Also set it up in the cache so the capturing will start opl3
                CacheWrite(0x105, (short)1);
                break;
            }
        }
    }

    private static Module module = null;

    static private final Mixer.MIXER_Handler OPL_CallBack = new Mixer.MIXER_Handler() {
        public void call(/*Bitu*/int len) {
            module.handler.Generate( module.mixerChan, len );
            //Disable the sound generation after 30 seconds of silence
            if ((Pic.PIC_Ticks - module.lastUsed) > 30000) {
                /*Bitu*/int i;
                for (i=0xb0;i<0xb9;i++) if ((module.cache[i] &0x20)!=0 || (module.cache[i+0x100] & 0x20)!=0) break;
                if (i==0xb9) module.mixerChan.Enable(false);
                else module.lastUsed = Pic.PIC_Ticks;
            }
        }
    };

    static final private IoHandler.IO_ReadHandler OPL_Read = new IoHandler.IO_ReadHandler() {
        public /*Bitu*/int call(/*Bitu*/int port, /*Bitu*/int iolen) {
            return module.PortRead( port, iolen );
        }
    };

    static final private IoHandler.IO_WriteHandler OPL_Write = new IoHandler.IO_WriteHandler() {
        public void call(/*Bitu*/int port, /*Bitu*/int val, /*Bitu*/int iolen) {
            module.PortWrite( port, (short)val, iolen );
        }
    };

    static public void OPL_Init(int oplmode) {
        Module.oplmode = oplmode;
        module = new Module();
    }

}
